package chabernac.beanutils;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import chabernac.beanutils.facade.BeanUtils;

public class CopyBean {
	private final Class source;
	private final Class target;
	private final BeanInfo sourceBeanInfo;
	private final BeanInfo targetBeanInfo;
	private final PropertyDescriptorCollection sourceDescriptors;
	private final PropertyDescriptorCollection targetDescriptors;

	public CopyBean(Class aSource, Class aTarget) throws IntrospectionException {
		super();
		source = aSource;
		target = aTarget;
		sourceBeanInfo = Introspector.getBeanInfo(aSource);
		targetBeanInfo = Introspector.getBeanInfo(aTarget);
		sourceDescriptors = new PropertyDescriptorCollection(sourceBeanInfo.getPropertyDescriptors());
		targetDescriptors = new PropertyDescriptorCollection(targetBeanInfo.getPropertyDescriptors());
	}

	private void copy(Collection aSourceCollection, Collection aTargetCollection, Class aTargetClass) throws InstantiationException,
			IllegalAccessException, IntrospectionException {
		for (Object theObj : aSourceCollection) {
			Object theNewObject = aTargetClass.newInstance();
			BeanUtils.copyBean(theObj, theNewObject);
			aTargetCollection.add(theNewObject);
		}
	}

	public void copy(Object aSource, Object aTarget) {
		for (PropertyDescriptor theDescripter : sourceDescriptors.getPropertyDescriptors()) {
			PropertyDescriptor theTargetDescriptor = targetDescriptors.findPropertyDescriptor(theDescripter.getName());
			if (theTargetDescriptor != null && theDescripter.getReadMethod() != null && theTargetDescriptor.getWriteMethod() != null) {
				try {
					Method theReadMethod = theDescripter.getReadMethod();
					Method theWriteMethod = theTargetDescriptor.getWriteMethod();

					Object theValue = theDescripter.getReadMethod().invoke(aSource, new Object[] {});
					if (Utils.isPrimitive(theReadMethod.getReturnType()) || theValue == null) {
						theTargetDescriptor.getWriteMethod().invoke(aTarget, new Object[] { theValue });
					} else {
						// it's a complex type instantiate it and call beanutils again to
						// copy properties
						GenericObject theNewObject = getObject(theTargetDescriptor, aTarget);
						if (theNewObject.isCollection) {
							copy((Collection) theValue, (Collection) theNewObject.object, theNewObject.type);
						} else {
							BeanUtils.copyBean(theValue, theNewObject.object);
						}
						theTargetDescriptor.getWriteMethod().invoke(aTarget, new Object[] { theNewObject.object });
					}
				} catch (Exception e) {
					throw new CouldNotCopyException("Error occured while trying to copy property '" + theDescripter.getName() + " from class '"
							+ source + "' to '" + target + "'", e);
				}
			}
		}
	}

	private GenericObject getObject(PropertyDescriptor aPropertyDescriptor, Object aTarget) throws IllegalAccessException,
			IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, InstantiationException {
		Object theValue = aPropertyDescriptor.getReadMethod().invoke(aTarget, new Object[] {});
		if (theValue != null) {
			return new GenericObject(theValue, null, false);
		}

		Class theTargetClass = aPropertyDescriptor.getWriteMethod().getParameterTypes()[0];

		if (List.class.isAssignableFrom(theTargetClass))
			return new GenericObject(new ArrayList(), getGenericType(aPropertyDescriptor.getWriteMethod()), true);
		if (Set.class.isAssignableFrom(theTargetClass))
			return new GenericObject(new HashSet(), getGenericType(aPropertyDescriptor.getWriteMethod()), true);

		if (theTargetClass.isInterface()) {
			throw new UnsupportedOperationException("CopyBean is not yet able to handle implementation of " + theTargetClass.getName());
		}

		Constructor theConstructor = theTargetClass.getConstructor(new Class[] {});
		if (theConstructor != null) {
			return new GenericObject(theConstructor.newInstance(null), null, false);
		}
		return null;
	}

	private Class getGenericType(Method aMethod) {
		return (Class) ((ParameterizedType) aMethod.getGenericParameterTypes()[0]).getActualTypeArguments()[0];
	}

	private class GenericObject {
		public Object object;
		public Class type;
		public boolean isCollection;

		public GenericObject(Object aObject, Class aType, boolean isCollection) {
			super();
			object = aObject;
			type = aType;
			this.isCollection = isCollection;
		}
	}
}
